use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc};

use oso::{FromPolar, Oso, OsoError, PolarClass, ToPolar};

#[derive(Clone, PolarClass)]
pub struct User {
    pub username: String,
    #[polar(attribute)]
    pub roles: Vec<RepositoryRole>,
}

#[derive(Clone, PolarClass)]
pub struct Repository {
    #[polar(attribute)]
    pub name: String,
    #[polar(attribute)]
    pub is_public: bool,
}

#[derive(Clone, PolarClass)]
pub struct RepositoryRole {
    #[polar(attribute)]
    pub name: String,
    #[polar(attribute)]
    pub repository: String,
}

impl User {
    pub fn new(username: impl Into<String>) -> Self {
        User {
            username: username.into(),
            roles: Vec::new(),
        }
    }

    pub fn add_role(&mut self, name: impl Into<String>, repository: impl Into<String>) {
        self.roles.push(RepositoryRole::new(name, repository));
    }
}

impl Repository {
    pub fn new(name: impl Into<String>, public: bool) -> Self {
        Repository {
            name: name.into(),
            is_public: public,
        }
    }
}

impl RepositoryRole {
    pub fn new(name: impl Into<String>, repository: impl Into<String>) -> Self {
        RepositoryRole {
            name: name.into(),
            repository: repository.into(),
        }
    }
}

pub struct Permission<U, UR, R> {
    oso: Arc<Oso>,
    _user: PhantomData<U>,
    _user_role: PhantomData<UR>,
    _repository: PhantomData<R>,
}

impl<U, UR, R> Permission<U, UR, R>
where
    U: PolarClass + ToPolar,
    UR: PolarClass,
    R: PolarClass + ToPolar,
{
    pub fn try_new() -> Result<Self, OsoError> {
        let mut oso = Oso::new();
        oso.register_class(U::get_polar_class_builder().build())?;
        oso.register_class(UR::get_polar_class_builder().build())?;
        oso.register_class(R::get_polar_class_builder().build())?;
        Permission::<U, UR, R>::load_polocies(&mut oso)?;
        Ok(Permission {
            oso: Arc::new(oso),
            _user: PhantomData,
            _user_role: PhantomData,
            _repository: PhantomData,
        })
    }

    pub fn is_allowed<A>(&self, actor: U, action: A, resource: R) -> Result<bool, OsoError>
    where
        A: ToPolar,
    {
        self.oso.is_allowed(actor, action, resource)
    }

    pub fn get_allowed_actions<A>(&self, actor: U, resource: R) -> Result<HashSet<A>, OsoError>
    where
        A: FromPolar + Eq + Hash,
    {
        self.oso.get_allowed_actions(actor, resource)
    }

    pub fn get_roles(&self, _actor: U, _resource: R) -> Result<HashSet<UR>, OsoError> {
        todo!()
    }

    fn load_polocies(oso: &mut Oso) -> Result<(), OsoError> {
        let base_dir = std::env::var("POLOCY_DIR")
            .unwrap_or_else(|_| format!("{}/rules", env!("CARGO_MANIFEST_DIR")));
        let files = std::fs::read_dir(base_dir)?
            .filter_map(|entry| entry.ok())
            .map(|entry| entry.path())
            .filter(|p| p.is_file() && p.extension() == Some(std::ffi::OsStr::new("polar")))
            .collect::<Vec<_>>();
        oso.load_files(files)?;
        Ok(())
    }
}
